﻿using System;
using System.Collections;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// Notifies listeners of dynamic changes, such as when items get added and removed or the whole list is refreshed.
    /// </summary>
    public interface INotifyCollectionChanged
    {
        /// <summary>
        /// Occurs when the content of a collection has changed.
        /// </summary>
        event EventHandler<NotifyCollectionChangedEventArgs> CollectionChanged;
    }

    /// <summary>
    /// Provides the data for the <see cref="INotifyCollectionChanged.CollectionChanged"/> event.
    /// </summary>
    public class NotifyCollectionChangedEventArgs : EventArgs
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class 
        /// that describes a <see cref="NotifyCollectionChangedAction.Reset"/> change.
        /// </summary>
        public NotifyCollectionChangedEventArgs()
        {
            Action = NotifyCollectionChangedAction.Reset;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class that describes a multi-item change.
        /// </summary>
        /// <param name="action">
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// The action that caused the event. This can be set to <see cref="NotifyCollectionChangedAction.Reset"/>,
        /// <see cref="NotifyCollectionChangedAction.Add"/> or <see cref="NotifyCollectionChangedAction.Remove"/>.
        /// </para>
        /// </param>
        /// <param name="changedItems">
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The items that are affected by the change.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentException">
        /// The <paramref name="action"/> is not one of <see cref="NotifyCollectionChangedAction.Reset"/>,
        /// <see cref="NotifyCollectionChangedAction.Add"/> or <see cref="NotifyCollectionChangedAction.Remove"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="action"/> is <see cref="NotifyCollectionChangedAction.Reset"/> 
        /// but the <paramref name="changedItems"/> is not <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="action"/> is one of <see cref="NotifyCollectionChangedAction.Add"/> or <see cref="NotifyCollectionChangedAction.Remove"/>,
        /// but the <paramref name="changedItems"/> argument is <see langword="null"/>.
        /// </exception>
        public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList changedItems)
        {
            if (!(action == NotifyCollectionChangedAction.Add || action == NotifyCollectionChangedAction.Reset || action == NotifyCollectionChangedAction.Remove))
                throw new ArgumentException("The action must be one of Add, Reset or Remove", "action");

            Action = action;

            if (action == NotifyCollectionChangedAction.Reset)
            {
                if (changedItems != null)
                    throw new ArgumentException("Reset action requires the changedItems argument to be null!", "action");

                NewItems = null;
                NewStartingIndex = -1;
            }
            else
            {
                if (changedItems == null)
                    throw new ArgumentNullException("changedItems");

                if (action == NotifyCollectionChangedAction.Add)
                {
                    NewItems = ArrayList.ReadOnly(changedItems);
                    NewStartingIndex = -1;
                }
                else
                {
                    OldItems = ArrayList.ReadOnly(changedItems);
                    OldStartingIndex = -1;
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class that describes
        /// a multi-item <see cref="NotifyCollectionChangedAction.Replace"/> action.
        /// </summary>
        /// <param name="action">
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// The action that caused the event. This must be <see cref="NotifyCollectionChangedAction.Replace"/>.
        /// </para>
        /// </param>
        /// <param name="newItems">
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The new items that are replacing the original items.
        /// </para>
        /// </param>
        /// <param name="oldItems">
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The original items that are replaced.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="newItems"/> or <paramref name="oldItems"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="action"/> is not <see cref="NotifyCollectionChangedAction.Replace"/>.
        /// </exception>
        public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems)
        {
            if (action != NotifyCollectionChangedAction.Replace)
                throw new ArgumentException("This constructor can only be used with the Replace action!", "action");
            if (newItems == null)
                throw new ArgumentNullException("newItems");
            if (oldItems == null)
                throw new ArgumentNullException("oldItems");

            Action = action;
            NewStartingIndex = -1;
            OldStartingIndex = -1;
            NewItems = ArrayList.ReadOnly(newItems);
            OldItems = ArrayList.ReadOnly(oldItems);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class that describes
        /// a multi-item <see cref="NotifyCollectionChangedAction.Replace"/> action.
        /// </summary>
        /// <param name="action">
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// The action that caused the event. This must be <see cref="NotifyCollectionChangedAction.Replace"/>.
        /// </para>
        /// </param>
        /// <param name="newItems">
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The new items that are replacing the original items.
        /// </para>
        /// </param>
        /// <param name="oldItems">
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The original items that are replaced.
        /// </para>
        /// </param>
        /// <param name="startingIndex">
        /// <para>
        /// The index of the first item of the items that are being replaced.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="newItems"/> or <paramref name="oldItems"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="action"/> is not <see cref="NotifyCollectionChangedAction.Replace"/>.
        /// </exception>
        public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex)
        {
            if (action != NotifyCollectionChangedAction.Replace)
                throw new ArgumentException("This constructor can only be used with the Replace action!", "action");
            if (newItems == null)
                throw new ArgumentNullException("newItems");
            if (oldItems == null)
                throw new ArgumentNullException("oldItems");

            Action = action;
            NewItems = ArrayList.ReadOnly(newItems);
            OldItems = ArrayList.ReadOnly(oldItems);
            NewStartingIndex = startingIndex;
            OldStartingIndex = startingIndex;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class that describes a one-item change.
        /// </summary>
        /// <param name="action">
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// The action that caused the event. This can be set to <see cref="NotifyCollectionChangedAction.Reset"/>,
        /// <see cref="NotifyCollectionChangedAction.Add"/> or <see cref="NotifyCollectionChangedAction.Remove"/>.
        /// </para>
        /// </param>
        /// <param name="changedItem">
        /// <para>
        /// Type: <see cref="System.Object"/>
        /// </para>
        /// <para>
        /// The item that is affected by the change.
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The index where the change occured.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentException">
        /// The <paramref name="action"/> is not one of <see cref="NotifyCollectionChangedAction.Reset"/>,
        /// <see cref="NotifyCollectionChangedAction.Add"/> or <see cref="NotifyCollectionChangedAction.Remove"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="action"/> is <see cref="NotifyCollectionChangedAction.Reset"/> and either
        /// <paramref name="changedItem"/> is not <see langword="null"/> or <paramref name="index"/> is not -1.
        /// </exception>
        public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object changedItem, int index)
        {
            if (((action != NotifyCollectionChangedAction.Add) && (action != NotifyCollectionChangedAction.Remove)) && (action != NotifyCollectionChangedAction.Reset))
                throw new ArgumentException("The action must be one of Add, Remove or Reset!", "action");

            Action = action;

            if (action == NotifyCollectionChangedAction.Reset)
            {
                if (changedItem != null)
                    throw new ArgumentException("The Reset action requires the changedItem argument to be null!", "action");

                if (index != -1)
                    throw new ArgumentException("The Reset action requires the index to be -1!", "action");

                NewItems = null;
                NewStartingIndex = -1;
            }
            else
            {
                if (action == NotifyCollectionChangedAction.Add)
                {
                    NewItems = ArrayList.ReadOnly(new object[] { changedItem });
                    NewStartingIndex = index;
                }
                else
                {
                    OldItems = ArrayList.ReadOnly(new object[] { changedItem });
                    OldStartingIndex = index;
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class that describes
        /// a one-item <see cref="NotifyCollectionChangedAction.Replace"/> action.
        /// </summary>
        /// <param name="action">
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// The action that caused the event. This must be <see cref="NotifyCollectionChangedAction.Replace"/>.
        /// </para>
        /// </param>
        /// <param name="oldItem">
        /// <para>
        /// Type: <see cref="System.Object"/>
        /// </para>
        /// <para>
        /// The original item that was replaced.
        /// </para>
        /// </param>
        /// <param name="newItem">
        /// <para>
        /// Type: <see cref="System.Object"/>
        /// </para>
        /// <para>
        /// The new item that replaced the previous item.
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The index at which the change occured.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentException">
        /// The <paramref name="action"/> is not <see cref="NotifyCollectionChangedAction.Replace"/>.
        /// </exception>
        public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
        {
            if (action != NotifyCollectionChangedAction.Replace)
                throw new ArgumentException("This constructor can only be used with the Replace action!", "action");

            Action = NotifyCollectionChangedAction.Replace;
            NewItems = ArrayList.ReadOnly(new object[] { newItem });
            OldItems = ArrayList.ReadOnly(new object[] { oldItem });
            NewStartingIndex = index;
            OldStartingIndex = index;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyCollectionChangedEventArgs"/> class that describes
        /// a one-item <see cref="NotifyCollectionChangedAction.Move"/> action.
        /// </summary>
        /// <param name="action">
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// The action that caused the event. This must be <see cref="NotifyCollectionChangedAction.Move"/>.
        /// </para>
        /// </param>
        /// <param name="changedItem">
        /// <para>
        /// Type: <see cref="System.Object"/>
        /// </para>
        /// <para>
        /// The item affected by the change
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The new index for the changed item.
        /// </para>
        /// </param>
        /// <param name="oldIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The old index for the changed item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentException">
        /// The <paramref name="action"/> is not <see cref="NotifyCollectionChangedAction.Move"/> or <paramref name="index"/> is less than 0.
        /// </exception>
        public NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object changedItem, int index, int oldIndex)
        {
            if (action != NotifyCollectionChangedAction.Move)
                 throw new ArgumentException("This constructor can only be used with the Move action!", "action");

            if (index < 0)
            {
                throw new ArgumentException("The index cannot be negative!", "index");
            }
            
            object[] newItems = new object[] { changedItem };
            
            Action = action;
            NewItems = ArrayList.ReadOnly(newItems);
            OldItems = ArrayList.ReadOnly(newItems);
            NewStartingIndex = index;
            OldStartingIndex = oldIndex;
        }
        
        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the action that caused the event.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="NotifyCollectionChangedAction"/>
        /// </para>
        /// <para>
        /// A <see cref="NotifyCollectionChangedAction"/> value that describes the action that caused the event.
        /// </para>
        /// </value>
        public NotifyCollectionChangedAction Action
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the list of new items involved in the change.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The list of new items involved in the change.
        /// </para>
        /// </value>
        public IList NewItems
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the index at which the change occured.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The index at which the change occured.
        /// </para>
        /// </value>
        public int NewStartingIndex
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the list of items affected by <see cref="NotifyCollectionChangedAction.Replace"/>
        /// <see cref="NotifyCollectionChangedAction.Remove"/> or <see cref="NotifyCollectionChangedAction.Move"/> action.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="IList"/>
        /// </para>
        /// <para>
        /// The list of items affected by <see cref="NotifyCollectionChangedAction.Replace"/>
        /// <see cref="NotifyCollectionChangedAction.Remove"/> or <see cref="NotifyCollectionChangedAction.Move"/> action.
        /// </para>
        /// </value>
        public IList OldItems
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the index at which <see cref="NotifyCollectionChangedAction.Replace"/>
        /// <see cref="NotifyCollectionChangedAction.Remove"/> or <see cref="NotifyCollectionChangedAction.Move"/> action occured.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The index at which <see cref="NotifyCollectionChangedAction.Replace"/>
        /// <see cref="NotifyCollectionChangedAction.Remove"/> or <see cref="NotifyCollectionChangedAction.Move"/> action occured.
        /// </para>
        /// </value>
        public int OldStartingIndex
        {
            get;
            private set;
        }

        #endregion
    }

    /// <summary>
    /// Describes the action that caused a <see cref="INotifyCollectionChanged.CollectionChanged"/> event.
    /// </summary>
    public enum NotifyCollectionChangedAction
    {
        /// <summary>
        /// One or more items were added to the collection.
        /// </summary>
        Add,
        /// <summary>
        /// One or more items were removed from the collection.
        /// </summary>
        Remove,
        /// <summary>
        /// One or more items were replaced in the collection.
        /// </summary>
        Replace,
        /// <summary>
        /// One or more items were moved within the collection.
        /// </summary>
        Move,
        /// <summary>
        /// The content of the collection changed dramatically.
        /// </summary>
        Reset
    }
}
